package org.swing.utility.input.common;

import java.awt.Component;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.Insets;
import java.awt.KeyboardFocusManager;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.KeyEvent;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.text.BreakIterator;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;

/**
 * Contribution from NetBeans: Issue #319-swingx.
 * <p>
 * 
 * PENDING: need to reconcile with OS, JVM... added as-is because needed the
 * shortcut handling to fix #
 * 
 * @author apple
 */
public class Utilities {
	private Utilities() {
	}

	private static final int CTRL_WILDCARD_MASK = 32768;
	private static final int ALT_WILDCARD_MASK = CTRL_WILDCARD_MASK * 2;

	/** Operating system is Windows NT. */
	public static final int OS_WINNT = 1 << 0;

	/** Operating system is Windows 95. */
	public static final int OS_WIN95 = OS_WINNT << 1;

	/** Operating system is Windows 98. */
	public static final int OS_WIN98 = OS_WIN95 << 1;

	/** Operating system is Solaris. */
	public static final int OS_SOLARIS = OS_WIN98 << 1;

	/** Operating system is Linux. */
	public static final int OS_LINUX = OS_SOLARIS << 1;

	/** Operating system is HP-UX. */
	public static final int OS_HP = OS_LINUX << 1;

	/** Operating system is IBM AIX. */
	public static final int OS_AIX = OS_HP << 1;

	/** Operating system is SGI IRIX. */
	public static final int OS_IRIX = OS_AIX << 1;

	/** Operating system is Sun OS. */
	public static final int OS_SUNOS = OS_IRIX << 1;

	/** Operating system is Compaq TRU64 Unix */
	public static final int OS_TRU64 = OS_SUNOS << 1;

	/** Operating system is OS/2. */
	public static final int OS_OS2 = OS_TRU64 << 2;

	/** Operating system is Mac. */
	public static final int OS_MAC = OS_OS2 << 1;

	/** Operating system is Windows 2000. */
	public static final int OS_WIN2000 = OS_MAC << 1;

	/** Operating system is Compaq OpenVMS */
	public static final int OS_VMS = OS_WIN2000 << 1;

	/**
	 * Operating system is one of the Windows variants but we don't know which
	 * one it is
	 */
	public static final int OS_WIN_OTHER = OS_VMS << 1;

	/** Operating system is unknown. */
	public static final int OS_OTHER = OS_WIN_OTHER << 1;

	/**
	 * Operating system is FreeBSD
	 * 
	 * @since 4.50
	 */
	public static final int OS_FREEBSD = OS_OTHER << 1;

	/** A mask for Windows platforms. */
	public static final int OS_WINDOWS_MASK = OS_WINNT | OS_WIN95 | OS_WIN98
			| OS_WIN2000 | OS_WIN_OTHER;

	/** A mask for Unix platforms. */
	public static final int OS_UNIX_MASK = OS_SOLARIS | OS_LINUX | OS_HP
			| OS_AIX | OS_IRIX | OS_SUNOS | OS_TRU64 | OS_MAC | OS_FREEBSD;

	/** A height of the windows's taskbar */
	public static final int TYPICAL_WINDOWS_TASKBAR_HEIGHT = 27;

	/** A height of the Mac OS X's menu */
	private static final int TYPICAL_MACOSX_MENU_HEIGHT = 24;

	private static int operatingSystem = -1;

	/**
	 * reference to map that maps allowed key names to their values (String,
	 * Integer) and reference to map for mapping of values to their names
	 */
	private static Reference<Object> namesAndValues;

	/**
	 * Get the operating system.
	 * 
	 * @return one of the <code>OS_*</code> constants (such as {@link #OS_WINNT}
	 *         )
	 */
	public static int getOperatingSystem() {
		if (operatingSystem == -1) {
			String osName = System.getProperty("os.name");

			if ("Windows NT".equals(osName)) { // NOI18N
				operatingSystem = OS_WINNT;
			} else if ("Windows 95".equals(osName)) { // NOI18N
				operatingSystem = OS_WIN95;
			} else if ("Windows 98".equals(osName)) { // NOI18N
				operatingSystem = OS_WIN98;
			} else if ("Windows 2000".equals(osName)) { // NOI18N
				operatingSystem = OS_WIN2000;
			} else if (osName.startsWith("Windows ")) { // NOI18N
				operatingSystem = OS_WIN_OTHER;
			} else if ("Solaris".equals(osName)) { // NOI18N
				operatingSystem = OS_SOLARIS;
			} else if (osName.startsWith("SunOS")) { // NOI18N
				operatingSystem = OS_SOLARIS;
			}
			// JDK 1.4 b2 defines os.name for me as "Redhat Linux" -jglick
			else if (osName.endsWith("Linux")) { // NOI18N
				operatingSystem = OS_LINUX;
			} else if ("HP-UX".equals(osName)) { // NOI18N
				operatingSystem = OS_HP;
			} else if ("AIX".equals(osName)) { // NOI18N
				operatingSystem = OS_AIX;
			} else if ("Irix".equals(osName)) { // NOI18N
				operatingSystem = OS_IRIX;
			} else if ("SunOS".equals(osName)) { // NOI18N
				operatingSystem = OS_SUNOS;
			} else if ("Digital UNIX".equals(osName)) { // NOI18N
				operatingSystem = OS_TRU64;
			} else if ("OS/2".equals(osName)) { // NOI18N
				operatingSystem = OS_OS2;
			} else if ("OpenVMS".equals(osName)) { // NOI18N
				operatingSystem = OS_VMS;
			} else if (osName.equals("Mac OS X")) { // NOI18N
				operatingSystem = OS_MAC;
			} else if (osName.startsWith("Darwin")) { // NOI18N
				operatingSystem = OS_MAC;
			} else if (osName.toLowerCase(Locale.US).startsWith("freebsd")) { // NOI18N
				operatingSystem = OS_FREEBSD;
			} else {
				operatingSystem = OS_OTHER;
			}
		}
		return operatingSystem;
	}

	/**
	 * Finds out the monitor where the user currently has the input focus. This
	 * method is usually used to help the client code to figure out on which
	 * monitor it should place newly created windows/frames/dialogs.
	 * 
	 * @return the GraphicsConfiguration of the monitor which currently has the
	 *         input focus
	 */
	private static GraphicsConfiguration getCurrentGraphicsConfiguration() {
		Component focusOwner = KeyboardFocusManager
				.getCurrentKeyboardFocusManager().getFocusOwner();
		if (focusOwner != null) {
			Window w = SwingUtilities.getWindowAncestor(focusOwner);
			if (w != null) {
				return w.getGraphicsConfiguration();
			}
		}

		return GraphicsEnvironment.getLocalGraphicsEnvironment()
				.getDefaultScreenDevice().getDefaultConfiguration();
	}

	/**
	 * Returns the usable area of the screen where applications can place its
	 * windows. The method subtracts from the screen the area of taskbars,
	 * system menus and the like. The screen this method applies to is the one
	 * which is considered current, ussually the one where the current input
	 * focus is.
	 * 
	 * @return the rectangle of the screen where one can place windows
	 * 
	 * @since 2.5
	 */
	public static Rectangle getUsableScreenBounds() {
		return getUsableScreenBounds(getCurrentGraphicsConfiguration());
	}

	/**
	 * Returns the usable area of the screen where applications can place its
	 * windows. The method subtracts from the screen the area of taskbars,
	 * system menus and the like.
	 * 
	 * @param gconf
	 *            the GraphicsConfiguration of the monitor
	 * @return the rectangle of the screen where one can place windows
	 * 
	 * @since 2.5
	 */
	public static Rectangle getUsableScreenBounds(GraphicsConfiguration gconf) {
		if (gconf == null) {
			gconf = GraphicsEnvironment.getLocalGraphicsEnvironment()
					.getDefaultScreenDevice().getDefaultConfiguration();
		}

		Rectangle bounds = new Rectangle(gconf.getBounds());

		String str;

		str = System.getProperty("netbeans.screen.insets"); // NOI18N

		if (str != null) {
			StringTokenizer st = new StringTokenizer(str, ", "); // NOI18N

			if (st.countTokens() == 4) {
				try {
					bounds.y = Integer.parseInt(st.nextToken());
					bounds.x = Integer.parseInt(st.nextToken());
					bounds.height -= (bounds.y + Integer.parseInt(st
							.nextToken()));
					bounds.width -= (bounds.x + Integer
							.parseInt(st.nextToken()));
				} catch (NumberFormatException ex) {
					Logger.getAnonymousLogger().log(Level.WARNING, null, ex);
				}
			}

			return bounds;
		}

		str = System.getProperty("netbeans.taskbar.height"); // NOI18N

		if (str != null) {
			bounds.height -= Integer.getInteger(str, 0).intValue();

			return bounds;
		}

		try {
			Toolkit toolkit = Toolkit.getDefaultToolkit();
			Insets insets = toolkit.getScreenInsets(gconf);
			bounds.y += insets.top;
			bounds.x += insets.left;
			bounds.height -= (insets.top + insets.bottom);
			bounds.width -= (insets.left + insets.right);
		} catch (Exception ex) {
			Logger.getAnonymousLogger().log(Level.WARNING, null, ex);
		}

		return bounds;
	}

	/**
	 * Initialization of the names and values
	 * 
	 * @return array of two hashmaps first maps allowed key names to their
	 *         values (String, Integer) and second hashtable for mapping of
	 *         values to their names (Integer, String)
	 */
	private static synchronized HashMap[] initNameAndValues() {
		if (namesAndValues != null) {
			HashMap[] arr = (HashMap[]) namesAndValues.get();

			if (arr != null) {
				return arr;
			}
		}

		Field[] fields;
		// JW - fix Issue #353-swingx: play nicer inside sandbox.
		try {
			fields = KeyEvent.class.getDeclaredFields();
			// fields = KeyEvent.class.getFields();
		} catch (SecurityException e) {
			// JW: need to do better? What are the use-cases where we don't have
			// any access to the fields?
			fields = new Field[0];
		}

		HashMap<String, Integer> names = new HashMap<String, Integer>(
				((fields.length * 4) / 3) + 5, 0.75f);
		HashMap<Integer, String> values = new HashMap<Integer, String>(
				((fields.length * 4) / 3) + 5, 0.75f);

		for (int i = 0; i < fields.length; i++) {
			if (Modifier.isStatic(fields[i].getModifiers())) {
				String name = fields[i].getName();

				if (name.startsWith("VK_")) { // NOI18N

					// exclude VK
					name = name.substring(3);

					try {
						int numb = fields[i].getInt(null);
						Integer value = new Integer(numb);
						names.put(name, value);
						values.put(value, name);
					} catch (IllegalArgumentException ex) {
					} catch (IllegalAccessException ex) {
					}
				}
			}
		}

		if (names.get("CONTEXT_MENU") == null) { // NOI18N

			Integer n = new Integer(0x20C);
			names.put("CONTEXT_MENU", n); // NOI18N
			values.put(n, "CONTEXT_MENU"); // NOI18N

			n = new Integer(0x20D);
			names.put("WINDOWS", n); // NOI18N
			values.put(n, "WINDOWS"); // NOI18N
		}

		HashMap[] arr = { names, values };

		namesAndValues = new SoftReference<Object>(arr);

		return arr;
	}

	/**
	 * Converts a Swing key stroke descriptor to a familiar Emacs-like name.
	 * 
	 * @param stroke
	 *            key description
	 * @return name of the key (e.g. <code>CS-F1</code> for
	 *         control-shift-function key one)
	 * @see #stringToKey
	 */
	public static String keyToString(KeyStroke stroke) {
		StringBuffer sb = new StringBuffer();

		// add modifiers that must be pressed
		if (addModifiers(sb, stroke.getModifiers())) {
			sb.append('-');
		}

		HashMap[] namesAndValues = initNameAndValues();

		String c = (String) namesAndValues[1].get(new Integer(stroke
				.getKeyCode()));

		if (c == null) {
			sb.append(stroke.getKeyChar());
		} else {
			sb.append(c);
		}

		return sb.toString();
	}

	/**
	 * Construct a new key description from a given universal string
	 * description. Provides mapping between Emacs-like textual key descriptions
	 * and the <code>KeyStroke</code> object used in Swing.
	 * <P>
	 * This format has following form:
	 * <P>
	 * <code>[C][A][S][M]-<em>identifier</em></code>
	 * <p>
	 * Where:
	 * <UL>
	 * <LI> <code>C</code> stands for the Control key
	 * <LI> <code>A</code> stands for the Alt key
	 * <LI> <code>S</code> stands for the Shift key
	 * <LI> <code>M</code> stands for the Meta key
	 * </UL>
	 * The format also supports two wildcard codes, to support differences in
	 * platforms. These are the preferred choices for registering keystrokes,
	 * since platform conflicts will automatically be handled:
	 * <UL>
	 * <LI> <code>D</code> stands for the default menu accelerator - the Control
	 * key on most platforms, the Command (meta) key on Macintosh</LI>
	 * <LI> <code>O</code> stands for the alternate accelerator - the Alt key on
	 * most platforms, the Ctrl key on Macintosh (Macintosh uses Alt as a
	 * secondary shift key for composing international characters - if you bind
	 * Alt-8 to an action, a mac user with a French keyboard will not be able to
	 * type the <code>[</code> character, which is a significant handicap</LI>
	 * </UL>
	 * If you use the wildcard characters, and specify a key which will conflict
	 * with keys the operating system consumes, it will be mapped to whichever
	 * choice can work - for example, on Macintosh, Command-Q is always consumed
	 * by the operating system, so <code>D-Q</code> will always map to
	 * Control-Q.
	 * <p>
	 * Every modifier before the hyphen must be pressed. <em>identifier</EM> can
	 * be any text constant from {@link KeyEvent} but without the leading
	 * <code>VK_</code> characters. So {@link KeyEvent#VK_ENTER} is described as
	 * <code>ENTER</code>.
	 * 
	 * @param s
	 *            the string with the description of the key
	 * @return key description object, or <code>null</code> if the string does
	 *         not represent any valid key
	 */
	public static KeyStroke stringToKey(String s) {
		StringTokenizer st = new StringTokenizer(s.toUpperCase(Locale.ENGLISH),
				"-", true); // NOI18N

		int needed = 0;

		HashMap names = initNameAndValues()[0];

		int lastModif = -1;

		try {
			for (;;) {
				String el = st.nextToken();

				// required key
				if (el.equals("-")) { // NOI18N

					if (lastModif != -1) {
						needed |= lastModif;
						lastModif = -1;
					}

					continue;
				}

				// if there is more elements
				if (st.hasMoreElements()) {
					// the text should describe modifiers
					lastModif = readModifiers(el);
				} else {
					// last text must be the key code
					Integer i = (Integer) names.get(el);
					boolean wildcard = (needed & CTRL_WILDCARD_MASK) != 0;

					// Strip out the explicit mask - KeyStroke won't know
					// what to do with it
					needed = needed & ~CTRL_WILDCARD_MASK;

					boolean macAlt = (needed & ALT_WILDCARD_MASK) != 0;
					needed = needed & ~ALT_WILDCARD_MASK;

					if (i != null) {
						// #26854 - Default accelerator should be Command on mac
						if (wildcard) {
							needed |= getMenuShortCutKeyMask();

							if ((getOperatingSystem() & OS_MAC) != 0) {
								if (!usableKeyOnMac(i.intValue(), needed)) {
									needed &= ~getMenuShortCutKeyMask();
									needed |= KeyEvent.CTRL_MASK;
								}
							}
						}

						if (macAlt) {
							if (getOperatingSystem() == OS_MAC) {
								needed |= KeyEvent.CTRL_MASK;
							} else {
								needed |= KeyEvent.ALT_MASK;
							}
						}

						return KeyStroke.getKeyStroke(i.intValue(), needed);
					} else {
						return null;
					}
				}
			}
		} catch (NoSuchElementException ex) {
			return null;
		}
	}

	/**
	 * need to guard against headlessExceptions when testing.
	 * 
	 * @return the acceletor mask for shortcuts.
	 */
	private static int getMenuShortCutKeyMask() {
		if (GraphicsEnvironment.isHeadless()) {
			return ((getOperatingSystem() & OS_MAC) != 0) ? KeyEvent.META_MASK
					: KeyEvent.CTRL_MASK;
		}

		return Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
	}

	private static boolean usableKeyOnMac(int key, int mask) {
		// All permutations fail for Q except ctrl
		if (key == KeyEvent.VK_Q) {
			return false;
		}

		boolean isMeta = ((mask & KeyEvent.META_MASK) != 0)
				|| ((mask & KeyEvent.CTRL_DOWN_MASK) != 0);

		boolean isAlt = ((mask & KeyEvent.ALT_MASK) != 0)
				|| ((mask & KeyEvent.ALT_DOWN_MASK) != 0);

		boolean isOnlyMeta = isMeta
				&& ((mask & ~(KeyEvent.META_DOWN_MASK | KeyEvent.META_MASK)) == 0);

		// Mac OS consumes keys Command+ these keys - the app will never see
		// them, so CTRL should not be remapped for these
		if (isOnlyMeta) {
			return (key != KeyEvent.VK_H) && (key != KeyEvent.VK_SPACE)
					&& (key != KeyEvent.VK_TAB);
		} else
			return !((key == KeyEvent.VK_D) && isMeta && isAlt);
	}

	/**
	 * Convert a space-separated list of Emacs-like key binding names to a list
	 * of Swing key strokes.
	 * 
	 * @param s
	 *            the string with keys
	 * @return array of key strokes, or <code>null</code> if the string
	 *         description is not valid
	 * @see #stringToKey
	 */
	public static KeyStroke[] stringToKeys(String s) {
		StringTokenizer st = new StringTokenizer(s.toUpperCase(Locale.ENGLISH),
				" "); // NOI18N
		ArrayList<KeyStroke> arr = new ArrayList<KeyStroke>();

		while (st.hasMoreElements()) {
			s = st.nextToken();

			KeyStroke k = stringToKey(s);

			if (k == null) {
				return null;
			}

			arr.add(k);
		}

		return arr.toArray(new KeyStroke[arr.size()]);
	}

	/**
	 * Adds characters for modifiers to the buffer.
	 * 
	 * @param buf
	 *            buffer to add to
	 * @param modif
	 *            modifiers to add (KeyEvent.XXX_MASK)
	 * @return true if something has been added
	 */
	private static boolean addModifiers(StringBuffer buf, int modif) {
		boolean b = false;

		if ((modif & KeyEvent.CTRL_MASK) != 0) {
			buf.append("C"); // NOI18N
			b = true;
		}

		if ((modif & KeyEvent.ALT_MASK) != 0) {
			buf.append("A"); // NOI18N
			b = true;
		}

		if ((modif & KeyEvent.SHIFT_MASK) != 0) {
			buf.append("S"); // NOI18N
			b = true;
		}

		if ((modif & KeyEvent.META_MASK) != 0) {
			buf.append("M"); // NOI18N
			b = true;
		}

		if ((modif & CTRL_WILDCARD_MASK) != 0) {
			buf.append("D");
			b = true;
		}

		if ((modif & ALT_WILDCARD_MASK) != 0) {
			buf.append("O");
			b = true;
		}

		return b;
	}

	/**
	 * Reads for modifiers and creates integer with required mask.
	 * 
	 * @param s
	 *            string with modifiers
	 * @return integer with mask
	 * @exception NoSuchElementException
	 *                if some letter is not modifier
	 */
	private static int readModifiers(String s) throws NoSuchElementException {
		int m = 0;

		for (int i = 0; i < s.length(); i++) {
			switch (s.charAt(i)) {
			case 'C':
				m |= KeyEvent.CTRL_MASK;
				break;

			case 'A':
				m |= KeyEvent.ALT_MASK;
				break;

			case 'M':
				m |= KeyEvent.META_MASK;
				break;

			case 'S':
				m |= KeyEvent.SHIFT_MASK;
				break;

			case 'D':
				m |= CTRL_WILDCARD_MASK;
				break;

			case 'O':
				m |= ALT_WILDCARD_MASK;
				break;

			default:
				throw new NoSuchElementException(s);
			}
		}

		return m;
	}

}
